﻿/*		Note from "TIME_ZONE_INFORMATION structure" http://msdn.microsoft.com/en-us/library/ms725481%28VS.85%29.aspx
 To select the correct day in the month, set the wYear member to zero, the wHour and wMinute members to the transition time, the wDayOfWeek member to the appropriate weekday, and the wDay member to indicate the occurrence of the day of the week within the month (1 to 5, where 5 indicates the final occurrence during the month if that day of the week does not occur 5 times).

Using this notation, specify 02:00 on the first Sunday in April as follows: wHour = 2, wMonth = 4, wDayOfWeek = 0, wDay = 1. Specify 02:00 on the last Thursday in October as follows: wHour = 2, wMonth = 10, wDayOfWeek = 4, wDay = 5.

If the wYear member is not zero, the transition date is absolute; it will only occur one time. Otherwise, it is a relative date that occurs yearly.
 */

using System;
using System.ComponentModel.DataAnnotations;
using System.Xml.Serialization;

namespace SLTimeZoneHelper
{
   [XmlRoot("R")]
	public sealed class TimeZoneRule
	{
		[Display(AutoGenerateField = false)]
      [XmlIgnore]
		public Guid ID { get; set; }

		[Display(AutoGenerateField = false)]
      [XmlIgnore]      
		public string SchemaName { get; set; }

		[Display(Order = 0)]
		public Guid TimeZoneDefinitionId { get; set; }

		public int Bias { get; set; }

		public int DaylightBias { get; set; }

		public int DaylightDay { get; set; }

		public int DaylightDayOfWeek { get; set; }

		public int DaylightHour { get; set; }

		public int DaylightMinute { get; set; }

		public int DaylightMonth { get; set; }

		public int DaylightSecond { get; set; }

		public int DaylightYear { get; set; }

		[Display(Order = 1)]
		public DateTime EffectiveDateTime { get; set; }

		public int StandardBias { get; set; }

		public int StandardDay { get; set; }

		public int StandardDayOfWeek { get; set; }

		public int StandardHour { get; set; }

		public int StandardMinute { get; set; }

		public int StandardMonth { get; set; }

		public int StandardSecond { get; set; }

		public int StandardYear { get; set; }

		[Display(Order = 2)]
		public int TimeZoneRuleVersionNumber { get; set; }

		#region logic

		public TimeSpan GetOffsetFor(DateTime dt)
		{
			if (StandardMonth == 0) //seems that this is UTC
				return TimeSpan.FromMinutes(0 - (Bias));

			if (IsDaylight(dt))
			{
				return TimeSpan.FromMinutes(0 - (Bias + DaylightBias));
			}
			return TimeSpan.FromMinutes(0 - (Bias + StandardBias));
		}

		private bool IsDaylight(DateTime dt)
		{
			if (DaylightMonth == 0) return false;
			
			if (StandardMonth > DaylightMonth)
			{
				if (dt < ConvertToDateTime(StandardYear == 0 ? dt.Year : StandardYear, StandardMonth, StandardDayOfWeek, StandardDay, StandardHour, StandardMinute, StandardSecond) &&
						dt >= ConvertToDateTime(DaylightYear == 0 ? dt.Year : DaylightYear, DaylightMonth, DaylightDayOfWeek, DaylightDay, DaylightHour, DaylightMinute, DaylightSecond))
					return true;							
			}
			else
			{
				if (dt < ConvertToDateTime(StandardYear == 0 ? dt.Year : StandardYear, StandardMonth, StandardDayOfWeek, StandardDay, StandardHour, StandardMinute, StandardSecond) &&					
					 dt >= ConvertToDateTime(DaylightYear == 0 ? dt.Year-1 : DaylightYear, DaylightMonth, DaylightDayOfWeek, DaylightDay, DaylightHour, DaylightMinute, DaylightSecond))
					return true;
			}
			return false;
		}

		/// <summary>
		/// standardDayOfWeek: 0 == Sunday
		/// </summary>
		/// <param name="year"></param>
		/// <param name="month"></param>
		/// <param name="dayOfWeek"></param>
		/// <param name="occurrenceDay"></param>
		/// <param name="hour"></param>
		/// <param name="minute"></param>
		/// <param name="second"></param>
		/// <returns></returns>
		private static DateTime ConvertToDateTime(int year, int month, int dayOfWeek, int occurrenceDay, int hour, int minute, int second)
		{
			DateTime dt = new DateTime(year, month, 1, hour, minute, second, DateTimeKind.Local);

			return DayOccurance(dt, dayOfWeek, occurrenceDay);
		}

		private static DateTime DayOccurance(DateTime dt, int dow, int occuranceNumber)
		{
			if (occuranceNumber <= 0 || occuranceNumber > 5)
				throw new ArgumentOutOfRangeException(@"occuranceNumber", "number must be between [1..5]");

			if (occuranceNumber == 5) //mean last occurance
				return LastDayOccurance(dt.AddMonths(1).AddDays(-1), dow);			

			int currDow = (int)dt.DayOfWeek;
			int expDow = dow;

			dt = dt.AddDays(7 * (occuranceNumber - 1) + Math.Abs(expDow - currDow) - (expDow>currDow?1:0));

			return dt;
		}

		private static DateTime LastDayOccurance(DateTime dt, int dow)
		{
			// Best case is that we are there already
			if (dow == (int)dt.DayOfWeek)
				return dt;

			int currDow = (int)dt.DayOfWeek;
			int expDow = dow;

			dt = currDow > expDow ? dt.AddDays(expDow - currDow) : dt.AddDays((expDow - currDow) - 7);

			return dt;
		}
		#endregion
	}
}
